import { Map } from "./types";
import { BaseObject } from "./object";

type Listener = (event: Event, data: any) => void;
type ListenerBin = { listener: Listener, thisTarget: any, data: any, priority: number };

export interface IEventDispatcher {
    hasEventListener(type: string): boolean;
    addEventListener(type: string, listener: Listener, thisTarget: any, data: any, priority: number): void;
    removeEventListener(type: string, listener: Listener): void;
    dispatchEvent(event: Event): void;
}

export class Event extends BaseObject {
    public static toString(): string {
        return "[class Event]";
    }

    public type: string = "";
    public data: any = null;
    public target: EventDispatcher;
    public currentTarget: EventDispatcher;

    public _onClear(): void {
        this.type = "";
        this.data = null;
    }

    public toString(): string {
        return "[object Event Type " + this.type + " Data " + this.data + "]";
    }

    public copyFrom(value: this): this {
        this.type = value.type
        this.data = value.data
        this.target = value.target;
        return this;
    }
}

export class EventDispatcher extends BaseObject implements IEventDispatcher {
    public static toString(): string {
        return "[class EventDispatcher]";
    }

    protected _dipatchLevel: number = 0;
    protected readonly _listenerBins: Map<ListenerBin[]> = {};

    public constructor() {
        super();
    }

    protected _onClear(): void {
        for (let i in this._listenerBins) {
            delete this._listenerBins[i];
        }

        this._dipatchLevel = 0;
    }

    protected _isSameListener(a: any, b: any): boolean {
        return a === b;
    }

    public hasEventListener(type: string): boolean {
        return type in this._listenerBins[type];
    }

    public addEventListener(type: string, listener: Listener, thisTarget: any, data: any = null, priority: number = 0): void {
        this.removeEventListener(type, listener);

        let listenerBins = this._listenerBins[type];
        if (listenerBins) {
            if (this._dipatchLevel !== 0) {
                this._listenerBins[type] = listenerBins = listenerBins.concat();
            }
        }
        else {
            this._listenerBins[type] = listenerBins = [];
        }

        listenerBins.push({ listener: listener, thisTarget: thisTarget, data: data, priority: priority });
        // sort
    }

    public removeEventListener(type: string, listener: Listener, data: any = null): void {
        let listenerBins = this._listenerBins[type];
        if (listenerBins) {
            if (this._dipatchLevel !== 0) {
                this._listenerBins[type] = listenerBins = listenerBins.concat();
            }

            for (let i = 0, l = listenerBins.length; i < l; ++i) {
                const listenerBin = listenerBins[i];
                if (listenerBin.listener === listener && this._isSameListener(listenerBin.data, data)) {
                    listenerBins.splice(i, 1);
                }
            }
        }
    }

    public removeAllEventListener(type?: string): void {
        if (type) {
            delete this._listenerBins[type];
        }
        else {
            for (let i in this._listenerBins) {
                delete this._listenerBins[i];
            }
        }
    }

    public dispatchEvent(event: Event): void {
        const type = event.type;
        const listenerBins = this._listenerBins[type];
        if (listenerBins) {
            event.target = this;
            if (!event.currentTarget) {
                event.currentTarget = event.target;
            }

            this._dipatchLevel++;
            for (let listenerBin of listenerBins) {
                listenerBin.listener.call(listenerBin.thisTarget, event, listenerBin.data);
            }
            this._dipatchLevel--;
        }
    }

    public dispatchEventWidth(type: string, data: any): void {
        const event = Event.create(Event);
        event.type = type;
        event.data = data;

        this.dispatchEvent(event);

        event.returnToPool();
    }
}